// ****************************************************************
// Copyright 2012, Stephan Burguchev
// e-mail: me@sburg.net
// ****************************************************************
// *
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Updater.Common;

namespace Updater.Sources
{
    public class WebSource : ISource
    {
        private const int BufferLength = 50000;
        private const int DefaultTimeout = 2 * 60 * 1000;

        public WebSource(string username, string password) : this()
        {
            if (username == null)
            {
                throw new ArgumentNullException("username");
            }

            if (password == null)
            {
                throw new ArgumentNullException("password");
            }

            this.Username = username;
            this.Password = password;
        }

        public WebSource()
        {
            ServicePointManager.ServerCertificateValidationCallback += this.validateRemoteCertificate;
        }

        public event RemoteCertificateValidationCallback ServerCertificateValidationCallback
        {
            add
            {
                ServicePointManager.ServerCertificateValidationCallback += value;
            }
            remove
            {
                ServicePointManager.ServerCertificateValidationCallback -= value;
            }
        }

        public bool AlwaysAcceptRemoteCertificate { get; set; }

        public string Password { get; set; }
        public string Username { get; set; }

        public void Download(Uri location, Stream outStream, Action<DownloadProgressInfo> progressChangedCallback = null)
        {
            if (location == null)
            {
                throw new ArgumentNullException("location");
            }

            if (outStream == null)
            {
                throw new ArgumentNullException("outStream");
            }

            try
            {
                WebRequest request = WebRequest.Create(location);
                request.Credentials = this.getBasicCredentials(location);

                using (WebResponse response = request.GetResponse())
                {
                    using (Stream data = response.GetResponseStream())
                    {
                        if (progressChangedCallback == null)
                        {
                            data.CopyTo(outStream);
                        }
                        else
                        {
                            long fullSizeInBytes = response.ContentLength;
                            var buffer = new byte[BufferLength];
                            int read;
                            long totalLength = 0;
                            while ((read = data.Read(buffer, 0, buffer.Length)) != 0)
                            {
                                totalLength += read;
                                outStream.Write(buffer, 0, read);
                                progressChangedCallback(new DownloadProgressInfo(totalLength, fullSizeInBytes));
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                throw new UpdaterSourceException("Unable to download the application update.", ex);
            }
        }

        public void Download(
            Dictionary<Uri, Stream> resourcesToDownload,
            Action<DownloadProgressInfo> progressChangedCallback = null)
        {
            if (resourcesToDownload == null)
            {
                throw new ArgumentNullException("resourcesToDownload");
            }

            try
            {
                var exceptions = new List<Exception>();
                var autos = new List<AutoResetEvent>();
                long fullSizeInBytes = 0;
                long downloadedInBytes = 0;
                foreach (var resource in resourcesToDownload)
                {
                    var auto = new AutoResetEvent(false);
                    autos.Add(auto);

                    Uri location = resource.Key;
                    Stream outStream = resource.Value;
                    WebRequest request = WebRequest.Create(location);
                    request.Credentials = this.getBasicCredentials(location);
                    IAsyncResult result = request.BeginGetResponse(
                        delegate(IAsyncResult ar)
                        {
                            try
                            {
                                using (WebResponse response = request.EndGetResponse(ar))
                                {
                                    // ReSharper disable AccessToModifiedClosure
                                    Interlocked.Add(ref fullSizeInBytes, response.ContentLength);

                                    using (Stream responseStream = response.GetResponseStream())
                                    {
                                        var buffer = new byte[BufferLength];
                                        int read;
                                        while ((read = responseStream.Read(buffer, 0, buffer.Length)) != 0)
                                        {
                                            Interlocked.Add(ref downloadedInBytes, read);
                                            if (progressChangedCallback != null)
                                            {
                                                progressChangedCallback(
                                                    new DownloadProgressInfo(downloadedInBytes, fullSizeInBytes));
                                            }

                                            outStream.Write(buffer, 0, read);
                                        }
                                    }
                                    // ReSharper restore AccessToModifiedClosure
                                }
                            }
                            catch (Exception ex)
                            {
                                lock (exceptions)
                                {
                                    exceptions.Add(ex);
                                }
                            }
                            finally
                            {
                                auto.Set();
                            }
                        },
                        null);

                    ThreadPool.RegisterWaitForSingleObject(
                        result.AsyncWaitHandle,
                        delegate(object state, bool timedOut)
                        {
                            if (timedOut)
                            {
                                request.Abort();
                                lock (exceptions)
                                {
                                    exceptions.Add(new TimeoutException("The update request timed out."));
                                }
                            }
                        },
                        null,
                        DefaultTimeout,
                        true);
                }

                WaitHandle.WaitAll(autos.Cast<WaitHandle>().ToArray());

                if (exceptions.Any())
                {
                    throw exceptions.First();
                }
            }
            catch (Exception ex)
            {
                throw new UpdaterSourceException("Unable to download the application update data.", ex);
            }
        }

        private CredentialCache getBasicCredentials(Uri prefix)
        {
            return new CredentialCache
                {
                    {
                        prefix,
                        "Basic",
                        new NetworkCredential(this.Username, this.Password)
                    }
                };
        }

        // callback used to validate the certificate in an SSL conversation
        private bool validateRemoteCertificate(
            object sender,
            X509Certificate cert,
            X509Chain chain,
            SslPolicyErrors policyErrors)
        {
            return this.AlwaysAcceptRemoteCertificate;
        }
    }
}
